﻿using System;
using System.Collections.Generic;
using System.Linq;
using DotNetNeural.Data.Base;
using DotNetNeural.Data.Utils;
using DotNetNeural.Metrics;
using DotNetNeural.Utils.Adapters;

namespace DotNetNeural.Projections
{
    public class KohonenProjection : IProjection
    {
        #region Constants

        public const int MinX = 0;
        public const int MaxX = 1000;
        public const int MinY = 0;
        public const int MaxY = 1000;
        public const int DefaultIterationsCount = 500;
        public const float MinSammonStress = 0.4f;

        #endregion

        public IMetric Metric { get; private set; }

        public float[][] VectorsMatrix { get; private set; }

        public float[][] Projections { get; private set; }

        public int IterationsCount { get; private set; }

        public KohonenProjection(IEnumerable<float[]> vectors)
            : this(vectors, DefaultIterationsCount, new DefaultMetric())
        {
        }

        public KohonenProjection(IEnumerable<float[]> vectors, int iterationsCount)
            : this(vectors, iterationsCount, new DefaultMetric())
        {
        }

        public KohonenProjection(IEnumerable<float[]> vectors, int iterationsCount, IMetric metric)
        {
            if (vectors == null || metric == null)
                throw new NullReferenceException("Illegal null-reference args");

            if (vectors.Count() == 0)
                throw new ArgumentException("Illegal empty vectors collection");

            if (iterationsCount <= 0)
                throw new ArgumentException("Iterations count must be positive");

            Metric = metric;
            IterationsCount = iterationsCount;
            Init(vectors);
        }

        #region IProjection Members

        public IEnumerable<Point2D> CalculateProjection()
        {
            int[] indicesI = Enumerable.Range(0, VectorsMatrix.Length).ToArray();
            int[] indicesJ = Enumerable.Range(0, VectorsMatrix.Length).ToArray();

            float stress = 1f;
            int iterationIndex = 1;

            do
            {
                indicesI = indicesI.Shuffle().ToArray();
                indicesJ = indicesJ.Shuffle().ToArray();

                for (int i = 0; i < indicesI.Length; ++i)
                {
                    float[] distancesI = distances[indicesI[i]];

                    float[] projectionsI = Projections[indicesI[i]];

                    for (int j = 0; j < indicesJ.Length; ++j)
                    {
                        if (indicesI[i] == indicesJ[j])
                            continue;

                        float[] projectionsJ = Projections[indicesJ[j]];

                        float dij = distancesI[indicesJ[j]];
                        float Dij = Metric.CalculateDistance(projectionsI, projectionsJ);

                        if (Dij == 0)
                            Dij = Single.MinValue;

                        float delta = GetLambda(iterationIndex) * (dij - Dij) / Dij;

                        for (int l = 0; l < projectionsJ.Length; ++l)
                        {
                            float correction =
                                delta * (projectionsI[l] - projectionsJ[l]);

                            projectionsI[l] += correction;
                            projectionsJ[l] -= correction;
                        }
                    }
                }

                stress = CalculateSammonStress();
                ++iterationIndex;
            }
            while (stress > MinSammonStress && iterationIndex < IterationsCount);

            var q = from p in Projections
                    select new Point2D(p[0], p[1]);

            return ScalePoints(q);
        }

        #endregion

        #region Private methods

        private float GetLambda(int iterationIndex)
        {
            return 0.1f;
        }

        #region Sammon's stress calculation

        private float CalculateSammonStress()
        {
            float sum = 0;
            int size = distances.Length;

            for (int i = 0; i < size; ++i)
            {
                for (int j = 0; j < size; ++j)
                {
                    if (i < j)
                    {
                        float a = Metric.CalculateDistance(distances[i], distances[j]);
                        float b = Metric.CalculateDistance(Projections[i], Projections[j]);

                        float upper = (a-b)*(a-b);

                        float lower = a;

                        sum += upper / lower;
                    }
                }
            }

            return sum * 1/CalculateOriginalSumDistance();
        }

        private float CalculateOriginalSumDistance()
        {
            float sum = 0;
            int size = distances.Length;

            for (int i = 0; i < size; ++i)
            {
                for (int j = 0; j < size; ++j)
                {
                    if (i < j)
                    {
                        sum += Metric.CalculateDistance(distances[i], distances[j]);
                    }
                }
            }

            return sum;
        }

        #endregion

        #region Scaling

        private Tuple<Interval<float>,Interval<float>> FindLimits(IEnumerable<Point2D> points)
        {
            float minX = Single.MaxValue;
            float maxX = Single.MinValue;
            float minY = Single.MaxValue;
            float maxY = Single.MinValue;

            foreach (var p in points)
            {
                if (p.X < minX)
                    minX = p.X;
                if (p.X > maxX)
                    maxX = p.X;
                if (p.Y < minY)
                    minY = p.Y;
                if (p.Y > maxY)
                    maxY = p.Y;
            }

            Interval<float> xLimits = new Interval<float>(minX, maxX);
            Interval<float> yLimits = new Interval<float>(minY, maxY);

            return new Tuple<Interval<float>, Interval<float>>(xLimits, yLimits);
        }

        private IEnumerable<Point2D> ScalePoints(IEnumerable<Point2D> points)
        {
            var limits = FindLimits(points);

            var limitX = limits.Item1;
            var limitY = limits.Item2;

            ScaleUtil scaleX = new ScaleUtil(MinX,MaxX);
            ScaleUtil scaleY = new ScaleUtil(MinY,MaxY);

            List<Point2D> result = new List<Point2D>();

            foreach (var p in points)
            {
                Point2D scaledPoint = new Point2D(
                    scaleX.Scale(limitX, p.X), 
                    scaleY.Scale(limitY, p.Y)
                    );
                result.Add(scaledPoint);
            }

            return result;
        }

        private Point2D ScalePoint(Point2D p)
        {
            return null;
        }

        #endregion

        #region Initialization

        private void Init(IEnumerable<float[]> sourceCollection)
        {
            InitVectorsMatrix(sourceCollection);
            InitProjections();
            InitDistanceMatrix();
        }

        private void InitVectorsMatrix(IEnumerable<float[]> sourceCollection)
        {
            VectorsMatrix = sourceCollection.ToArray();
        }

        private void InitProjections()
        {
            int vectorsCount = VectorsMatrix.Length;

            Projections = new float[vectorsCount][];
            Random rnd = new Random();


            for (int i = 0; i < vectorsCount; ++i)
            {
                Projections[i] = new float[2]; // 2-dim space

                Projections[i][0] = rnd.Next(MinX, MaxX); // x coordinate
                Projections[i][1] = rnd.Next(MinY, MaxY); // y coordinate
            }
        }

        private void InitDistanceMatrix()
        {
            MetricAdapter adapter = new MetricAdapter(Metric);
            float[,] dMatrix = adapter.CalculateDistancesMatrix(VectorsMatrix);

            int l1 = dMatrix.GetLength(0);
            int l2 = dMatrix.GetLength(1);

            distances = new float[l1][];

            for (int i = 0; i < l1; ++i)
            {
                distances[i] = new float[l2];
                for (int j = 0; j < l2; ++j)
                {
                    distances[i][j] = dMatrix[i, j];
                }
            }
        }

        #endregion

        #endregion

        #region Private fields

        float[][] distances;

        #endregion
    }
}
